#!/usr/bin/env python3
# -*- coding: utf-8 -*-

#if you want see a python function,should use 'help(funvname)'
help(abs)

#Calling function, if the parameter number is wrong, the Python interpreter will automatically check out and throw 'TypeError'.
#But if the parameter type is wrong, the Python interpreter will not be able to help us to check
#So, We should use 'isinstance()' to check the TypeError 

# ax2 + bx + c = 0        quadratic(a, b, c)    return x1 x2 
import math
def quadratic(a, b, c):
    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)) or not isinstance(c, (int, float)):
        raise TypeError('bad operand type')
    t = b*b-4*a*c
    if a == 0:
        return -(c/b)
    elif t < 0 :
        return
    elif t == 0 :
        return (-b+math.sqrt(t))/(2*a)
    else:
        return (-b+math.sqrt(t))/(2*a),(-b-math.sqrt(t))/(2*a)
        
print('2x^2 + 3x + 1 = 0, x =', quadratic(2, 3, 1))
print('1x^2 + 3x - 4 = 0, x =', quadratic(1, 3, -4))

#
def enroll(name, gender, age=6, city='Beijing'):
    print('\nname:', name)
    print('gender:', gender)
    print('age:', age)
    print('city:', city)

enroll('Bob', 'M', 7)
enroll('Adam', 'M', city='Tianjin')
print()

#A error example,is a trap
def add_end(L=[]):
    L.append('END')
    return L

print("add_end([1,2,3]) =",add_end([1,2,3]))    
#It looks very normal....

print("add_end([]) =",add_end())    
print("add_end([]) =",add_end())  
print("add_end([]) =",add_end())  
print()
#why? -- because L in the function definition has been created, is L equal to '[]', when calling the function, [] => ['END'] => ['END','END'] ....
#     -- So, remember, function of the default parameter must point to an immutable object
#     -- We should use 'None' to avoid this trap, 'None' is an immutable object
def add_end(L=None):
    if L is None:
        L = []
    L.append('END')
    return L

print("add_end([]) =",add_end())    
print("add_end([]) =",add_end())  
print("add_end([]) =",add_end())  
print()


# count the number of a^2 + b^2 + c^2 + ……
# Subconscious thought:incoming a list or tuple  to function
def calc(numbers):  
    sum = 0
    for n in numbers:
        sum = sum + n * n
    return sum

print('def calc(numbers):  ')
print('calc((1, 3, 5, 7)) = ', calc((1, 3, 5, 7)))   #incoming a tuple, note that the number of brackets



# ------------------------------------------------------------------------------------------------
# mutable parameter function
def calc(*numbers):  
    sum = 0
    for n in numbers:
        sum = sum + n * n
    return sum

#now, we should incoming  number of uncertainty
print('def calc(*numbers):  ')
print('calc(1, 3, 5, 7) = ', calc(1, 3, 5, 7))   
print('calc(1, 3, 5) = ', calc(1, 3, 5))  
print('calc() = ', calc())  

#If you already have a list or tuple
list = [1, 3, 5, 7]
tuple = (1, 3, 5, 7)

print('calc(*list) = ', calc(*list))  
print('calc(*tuple) = ', calc(*tuple))  
print()

# -----------------------------------------------------------------------------------------------

#Key parameters of the function
'''
可变参数允许你传入0个或任意个参数，这些可变参数在函数调用时自动组装为一个tuple。
而关键字参数允许你传入0个或任意个含参数名的参数，这些关键字参数在函数内部自动组装为一个dict。

关键字参数有什么用？它可以扩展函数的功能。比如，在person函数里，我们保证能接收到name和age这两个参数，但是，如果调用者愿意提供更多的参数，我们也能收到。
试想你正在做一个用户注册的功能，除了用户名和年龄是必填项外，其他都是可选项，利用关键字参数来定义这个函数就能满足注册的需求。
和可变参数类似，也可以先组装出一个dict，然后，把该dict转换为关键字参数传进去
'''
def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)
    #if no return value, automatic return 'None'
    
person('Michael', 30)
extra = {'city': 'Beijing', 'job': 'Engineer'}
person('Jack', 24, city=extra['city'], job=extra['job'])
person('Jack', 24, **extra)
"""
**extra表示把extra这个dict的所有key-value用关键字参数传入到函数的**kw参数，kw将获得一个dict
注意kw获得的dict是extra的一份拷贝，对kw的改动不会影响到函数外的extra。
"""


# ---------------------------------------------------------------------------------------------------

#Named keyword arguments of the function 命名关键字参数函数
"""
对于关键字参数，函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些，就需要在函数内部通过kw检查。
"""
def person(name, age, **kw):
    if 'city' in kw:
        # 有city参数
        pass
    if 'job' in kw:
        # 有job参数
        pass
    print('name:', name, 'age:', age, 'other:', kw)

#但是调用者仍可以传入不受限制的关键字参数：
person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456)

#如果要限制关键字参数的名字，就可以用命名关键字参数，例如，只接收city和job作为关键字参数。这种方式定义的函数如下：
def person(name, age, *, city, job):
    print(name, age, city, job)
#和关键字参数**kw不同，命名关键字参数需要一个特殊分隔符*，*后面的参数被视为命名关键字参数。
person('Jack', 24, city='Beijing', job='Engineer')

#命名关键字参数可以有缺省值，从而简化调用：
def person(name, age, *, city='Beijing', job):
    print(name, age, city, job)
#由于命名关键字参数city具有默认值，调用时，可不传入city参数：
person('Jack', 24, job='Engineer')


#如果函数定义中已经有了一个可变参数，后面跟着的命名关键字参数就不再需要一个特殊分隔符*了：
def person(name, age, *args, city, job):
    print(name, age, args, city, job)
"""
命名关键字参数必须传入参数名，这和位置参数不同。如果没有传入参数名，调用将报错
person('Jack', 24, 'Beijing', 'Engineer')
TypeError: person() missing 2 required keyword-only arguments: 'city' and 'job'
由于调用时缺少参数名city和job，Python解释器把这4个参数均视为位置参数，但person()函数仅接受2个位置参数。
"""
person('Jack', 24, 1,2,3,city='Beijing', job='Engineer')

#使用命名关键字参数时，要特别注意，如果没有可变参数，就必须加一个*作为特殊分隔符。如果缺少*，Python解释器将无法识别位置参数和命名关键字参数
def person(name, age, city, job):
    # 缺少 *，city和job被视为位置参数
    pass



    

# ---------------------------------------------------------------------------------------------------
#参数组合
"""
在Python中定义函数，可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数，这5种参数都可以组合使用。
但是请注意，参数定义的顺序必须是：必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
"""
def f1(a, b, c=0, *args, **kw):
    print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)

def f2(a, b, c=0, *, d, **kw):
    print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)

f1(1, 2)
f1(1, 2, c=3)
f1(1, 2, 3, 'a', 'b')
f1(1, 2, 3, 'a', 'b', x=99)
f2(1, 2, d=99, ext=None)

#最神奇的是通过一个tuple和dict，你也可以调用上述函数：
args = (1, 2, 3, 4)
kw = {'d': 99, 'x': '#'}
f1(*args, **kw)

args = (1, 2, 3)
kw = {'d': 88, 'x': '#'}
f2(*args, **kw)

#所以，对于任意函数，都可以通过类似func(*args, **kw)的形式调用它，无论它的参数是如何定义的。

"""
小结

Python的函数具有非常灵活的参数形态，既可以实现简单的调用，又可以传入非常复杂的参数。

默认参数一定要用不可变对象，如果是可变对象，程序运行时会有逻辑错误！

要注意定义可变参数和关键字参数的语法：

*args是可变参数，args接收的是一个tuple；

**kw是关键字参数，kw接收的是一个dict。

以及调用函数时如何传入可变参数和关键字参数的语法：

可变参数既可以直接传入：func(1, 2, 3)，又可以先组装list或tuple，再通过*args传入：func(*(1, 2, 3))；

关键字参数既可以直接传入：func(a=1, b=2)，又可以先组装dict，再通过**kw传入：func(**{'a': 1, 'b': 2})。

使用*args和**kw是Python的习惯写法，当然也可以用其他参数名，但最好使用习惯用法。

命名的关键字参数是为了限制调用者可以传入的参数名，同时可以提供默认值。

定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*，否则定义的将是位置参数。
"""



# ---------------------------------------------------------------------------------------------------
#递归函数recursive function

#fact(n) = n! = 1 x 2 x 3 x ... x (n-1) x n = (n-1)! x n = fact(n-1) x n
def fact(n):
    if n == 1:
        return 1;
    return fact(n-1)*n
                 
print(fact(10))
print(fact(100))
"""
递归函数的优点是定义简单，逻辑清晰。理论上，所有的递归函数都可以写成循环的方式，但循环的逻辑不如递归清晰。
使用递归函数需要注意防止栈溢出。
在计算机中，函数调用是通过栈（stack）这种数据结构实现的，每当进入一个函数调用，栈就会加一层栈帧，每当函数返回，栈就会减一层栈帧。
由于栈的大小不是无限的，所以，递归调用的次数过多，会导致栈溢出。

如print(fact(1000))  
RuntimeError: maximum recursion depth exceeded in comparison

解决递归调用栈溢出的方法是通过尾递归优化，事实上尾递归和循环的效果是一样的，所以，把循环看成是一种特殊的尾递归函数也是可以的。

尾递归是指，在函数返回的时候，调用自身本身，并且，return语句不能包含表达式。
这样，编译器或者解释器就可以把尾递归做优化，使递归本身无论调用多少次，都只占用一个栈帧，不会出现栈溢出的情况。

上面的fact(n)函数由于return n * fact(n - 1)引入了乘法表达式，所以就不是尾递归了。要改成尾递归方式，需要多一点代码，主要是要把每一步的乘积传入到递归函数中：
"""

def fact(n):
    return fact_iter(n,1)
    
def fact_iter(num,product):
    if num == 1:
        return product
    return fact_iter(num - 1, num+product)

print(fact(10))
print(fact(100))
"""
尾递归调用时，如果做了优化，栈不会增长，因此，无论多少次调用也不会导致栈溢出。

遗憾的是，大多数编程语言没有针对尾递归做优化，Python解释器也没有做优化，所以，即使把上面的fact(n)函数改成尾递归方式，也会导致栈溢出。

解决方法只有一个, 通过sys.setrecursionlimit(N)设置递归深度,默认为997
"""
#print(fact(998))
import sys
sys.setrecursionlimit(1500)
print(fact(998))

'''
小结

使用递归函数的优点是逻辑简单清晰，缺点是过深的调用会导致栈溢出。

针对尾递归优化的语言可以通过尾递归防止栈溢出。尾递归事实上和循环是等价的，没有循环语句的编程语言只能通过尾递归实现循环。

Python标准的解释器没有针对尾递归做优化，任何递归函数都存在栈溢出的问题。
'''











